from __future__ import division

from core import *


class Production(Object):
    duration = Attribute()
    input    = Attribute()
    output   = Attribute()
    create   = Attribute()
    
    inputs  = Plural('input')
    outputs = Plural('output')
    
    @property
    def input_types(self):
        return set(input.type for input in self.inputs)
    
    @property
    def output_types(self):            
        return set(output.type for output in self.outputs)
    
    def process_for(self, building):
        return lambda num: self.produce(building, num)
    
    def can_store_any_output_in(self, building):
        return any(building.num_storable(output) > 0 for output in self.output_types)
    
    def produce(self, building, num):
        if not self.can_store_any_output_in(building):
            return
        
        for input in self.inputs:
            num = min(num, building.number_left_of(input.type) // input.quantity)
        
        if num == 0:
            return
        
        for input in self.inputs:
            building.consume(input * num)
        
        if self.create is None:
            for output in self.outputs:
                building.create(output * num)
        else:
            self.create[1](self, building, num)
    
    def string_for(self, building):
        input  = ', '.join(str(good) for good in self.inputs)
        output = ', '.join(str(good) for good in self.outputs)
        middle = ''
        start  = ''
        
        if self.create is not None:
            output = self.create[0]
        
        if self.input is None:
            start = 'Creates '
        elif self.output is None and self.create is None:
            start = 'Consumes '
        else:
            middle = ' -> '
        
        return start + input + middle + output + (' (per %.1fd)' % self.duration)    
    
    def __str__(self):
        return self.string_for(None)


class TieredProduction(Production):
    base        = Attribute()
    enhancement = Attribute()
    
    bases        =  Plural('base')
    enhancements =  Plural('enhancement')
    
    @property
    def inputs(self):
        return self.bases + self.enhancements
    
    def best_goods_for(self, building, requirement):
        types = sorted(requirement.type.all_concrete_subtypes, key = lambda x: x.quality)
        found = []
        have  = 0
        
        for type in types:
            num = min(building.number_left_of(type), requirement.quantity - have)
            
            if num == 0:
                continue
            
            have += num
            
            found.append(type(num))
        
        return found
    
    def best_base(self, building):
        all = []
        
        for base in self.bases:
            best = self.best_goods_for(building, base)
            
            if not best:
                return None
            
            all.append(best)
        
        return tuple(all)
    
    def best_enhancements(self, building):
        all = []
        
        for enhancement in self.enhancements:
            best = self.best_goods_for(building, enhancement)
            
            if best is not None:
                all.append(best)
        
        return tuple(all)
    
    def quality_of(self, goods, total):
        if total == 0:
            return 0
        else:
            return sum(good.type.quality * good.quantity for good in goods) / total
    
    def choose_one_of(self, types, *factors):
        num = 0
        
        for factor in factors:
            while num < len(types) and self.chance(factor):
                num += 1
        
        return types[num]
    
    def probabilities_for(self, base, enhancements):
        b = self.quality_of(base,         sum(good.quantity for good in self.bases))
        e = self.quality_of(enhancements, sum(good.quantity for good in self.enhancements))
        
        return (b, e)
    
    def choose_outputs(self, base, enhancements):
        b, e = self.probabilities_for(base, enhancements)
        
        for output in self.outputs:
            types = sort(output.type.all_concrete_subtypes, key = lambda x: -x.quality)
            
            yield self.choose_one_of(types, b, e)
    
    def output_chances(self, building):
        base         = self.best_base(building)
        enhancements = self.best_enhancements(building)
        
        if base is None:
            return None
        
        b, e = self.probabilities_for(base, enhancements)
        
        chances = {}
        
        for output in self.outputs:
            num = len(output.type.all_concrete_subtypes)
            
            chances[output.type] = [pow(b * e, n) * (1 - b) * (1 - e) for n in range(num - 1)] + [pow(b * e, num - 1)]
        
        return chances
    
    def produce(self, building, num):
        if self.create is not None:
            raise AttributeError('Cannot use create with a tiered production')
        
        if num == 0 or not self.can_store_any_output_in(building):
            return
        
        base         = self.best_base(building)
        enhancements = self.best_enhancements(building)
        
        if base is None:
            return
        
        for output in self.choose_outputs(base, enhancements):
            building.create(output)
        
        for input in base + enhancements:
            building.consume(input)
        
        self.produce(num - 1)
    
    def string_for(self, building):
        if building is not None:
            chances = self.output_chances(building)
        else:
            chances = None
        
        input  = ', '.join(str(good) for good in self.inputs)
        middle = ''
        start  = ''
        
        if chances is None:
            output = ', '.join(str(good) for good in self.outputs)
        else:
            output = ', '.join(str(good) + ' [' + (', '.join('%d%%' % (chance * 100) for chance in chances[good.type])) + ']' for good in self.outputs)
        
        if not self.inputs:
            start = 'Creates '
        elif self.output is None:
            start = 'Consumes '
        else:
            middle = ' -> '
        
        return start + input + middle + output + (' (per %.1fd)' % self.duration)


class Workshop(Building):
    size     =_ (3)
    produces =_ (None)
    
    @property
    def stores(self):
        return (good(200) for good in self.all_types)
    
    def construction(self):
        for production in self.productions:
            self.process(production.process_for(self), self.calendar.per_day(1 / production.duration))
    
    @property
    def supply(self):
        types = set()
        
        for production in self.productions:
            types |= production.output_types
        
        return types
    
    @property
    def demand(self):
        types = set()
        
        for production in self.productions:
            types |= production.input_types
        
        return types
    
    @property
    def all_types(self):
        return self.supply | self.demand
    
    def number_needed_of(self, good):
        if good not in self.demand:
            return 0
        else:
            return self.storage.free_storage_for(good)
    
    def urgency_of(self, good):
        if good not in self.demand:
            return 0
        elif not self.storage.has(good):
            return 10000
        else:
            return max(self.storage.free_storage_for(good) - 50, 0)
    
    @property
    def productions(self):
        if self.produces is None:
            return ()
        elif isinstance(self.produces, tuple):
            return self.produces
        else:
            return (self.produces,)
    
    @property
    def state(self):
        state = {}
        
        for num, production in enumerate(self.productions):
            if len(self.productions) == 1:
                key = 'Production'
            else:
                key = 'Production #%d' % (num + 1)
            
            state[key] = (lambda production: lambda: production.string_for(self))(production)
        
        return state
